package ua.org.extjsbuilder.parser.runtime.builder;

import java.io.EOFException;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;

import ua.org.extjsbuilder.editors.JSKeywordsScanner;
import ua.org.extjsbuilder.parser.IInstruction;
import ua.org.extjsbuilder.parser.IToken;
import ua.org.extjsbuilder.parser.JSErrorHandler;
import ua.org.extjsbuilder.parser.JSObject;
import ua.org.extjsbuilder.parser.JSParseException;
import ua.org.extjsbuilder.parser.runtime.CommentToken;
import ua.org.extjsbuilder.parser.runtime.NumberLiteralToken;
import ua.org.extjsbuilder.parser.runtime.RegexpLiteralToken;
import ua.org.extjsbuilder.parser.runtime.StringLiteralToken;
import ua.org.extjsbuilder.parser.runtime.UnparsedToken;
import ua.org.extjsbuilder.parser.runtime.data.JSBoolean;
import ua.org.extjsbuilder.parser.runtime.data.JSConstants;
import ua.org.extjsbuilder.parser.runtime.data.JSNumber;
import ua.org.extjsbuilder.parser.runtime.data.JSRegExp;
import ua.org.extjsbuilder.parser.runtime.data.JSString;
import ua.org.extjsbuilder.parser.scanner.CharPattern;

abstract class AbstractInterpreter {
	protected static final Pattern JS_KEYWORDS = 
		Pattern.compile(JSKeywordsScanner.JS_KEYWORDS_PATTERN);
	protected static final CharPattern JS_INDEFIER = CharPattern.compile("$_")
		.or(CharPattern.ALLOW_LETTERS_ONLY).or(CharPattern.ALLOW_DIGITS_ONLY);
	
	protected UnparsedToken token;
	protected IToken literal;
	protected char[] data;
	protected int pos;
	protected int state;
	protected StringBuilder buf;
	protected IInstruction lastComplete;
	
	private JSErrorHandler handler;
	
	AbstractInterpreter(UnparsedToken start){
		this.token = start;
		this.pos = -1;
		this.data = start.getData();
		this.state = LValueStates.LEFT_START;
	}
	
	abstract boolean next() throws EOFException;
	
	boolean isComplete(){
		return state==LValueStates.LEFT_START;
	}
	
	protected boolean isInstructionEnd() throws EOFException{
		if(pos>=data.length){
			return true;
		}
		if(data[pos]==';'){
			pos++;
			return true;
		}
		for(int i=pos;i<data.length;i++){
			if(CharPattern.ALLOW_WHITE_SPACES.match(data[i])){
				continue;
			}
			if(data[i]==';'){
				pos = i+1;
				return true;
			}
			return false;
		}
		pos = data.length;
		return true;
	}
	
	IInstruction getLastComplete(){
		return lastComplete;
	}
	
	protected char nextChar() throws EOFException {
		pos++;
		if(data==null||pos>=data.length){
			do nextToken(true);
			while(data.length==0);
		}
		return data[pos];
	}
	
	protected boolean isNextLiteral(){
		IToken token = this.token.getNext();
		if(token==null)return false;
		if((token instanceof UnparsedToken)||
			(token instanceof RegexpLiteralToken)){
			return false;
		}
		return true;
	}

	protected IToken nextToken(boolean shouldBeUnparsed) throws EOFException {
		pos = 0;
		IToken token;
		if(data==null)token = literal;
		else token = this.token;
		do {
			token = token.getNext();
			if(token==null){
				throw new EOFException();
			}
		}
		while(token instanceof CommentToken);
		
		if(token instanceof UnparsedToken){
			this.token = (UnparsedToken) token;
		}
		else if(shouldBeUnparsed&&token instanceof RegexpLiteralToken){
			this.token = ((RegexpLiteralToken) token).toUnparsed();
		}
		else if(shouldBeUnparsed){
			throw new JSParseException("Unexpected literal",token.getLine());
		}
		else {
			this.data = null;
			literal = token;
			return null;
		}
		this.data = this.token.getData();
		
		return this.token;
	}
	
	protected void setState(AbstractInterpreter interpreter){
		setState(interpreter,false);
	}
	
	protected void setState(AbstractInterpreter interpreter,boolean deep) {
		if(interpreter==this)return;
		data = interpreter.data;
		literal = interpreter.literal;
		pos = interpreter.pos;
		token = interpreter.token;
		state = interpreter.state;
		buf = interpreter.buf;
		if(deep){
			lastComplete = interpreter.lastComplete;
		}
	}
	
	protected List<IInstruction> parseBlock(AbstractInterpreter intr) throws EOFException {
		List<IInstruction> block = new LinkedList<IInstruction>();
		intr.nextChar();
		intr.skipWhiteSpaces();
		intr.pos--;
		while(intr.next()){
			if(!intr.isComplete())continue;
			if(intr.getLastComplete()!=null){
				block.add(intr.getLastComplete());
			}
			intr.skipWhiteSpaces();
			if(intr.data.length<=intr.pos){
				intr.err("Unexpected end of the block");
			}
			if(intr.data[intr.pos]=='}'){
				break;
			}
			intr.pos--;
		}
		if(!intr.isComplete()){
			err("Syntax error");
		}
		setState(intr);
		return block;
	}
	
	protected void skipWhiteSpaces() throws EOFException {
		while(data.length<=pos||
			CharPattern.ALLOW_WHITE_SPACES
				.match(data[pos])){
			pos++;
			if(pos>=data.length){
				do {
					nextToken(false);
					if(data==null)return;
				}
				while(data.length==0);
			}
		}
	}
	
	protected void skipWhiteSpacesNoError() {
		try{
			ExpressionsInterpreter inr = new ExpressionsInterpreter(token, pos);
			inr.skipWhiteSpaces();
			setState(inr);
		}
		catch(EOFException ex){}
	}

	protected boolean isWhiteSpacesLine() {
		CharPattern pt = CharPattern.ALLOW_WHITE_SPACES;
		int p = pos<0?0:pos;
		int l = data.length;
		while(p<l){
			if(!pt.match(data[p])){
				return false;
			}
			p++;
		}
		return true;
	}
	
	protected void skipWhiteSpacesLine() throws EOFException {
		while(data.length>pos&&
				CharPattern.ALLOW_WHITE_SPACES
				.match(data[pos]))pos++;
	}
	
	protected JSObject getLiteral(){
		if(data!=null){
			StringBuilder buf = new StringBuilder();
			while(pos<data.length&&JS_INDEFIER.match(data[pos])){
				buf.append(data[pos++]);
			}
			if(JSBoolean.isBooleanLiteral(buf)){
				return JSBoolean.parseBoolean(buf);
			}
			if(JSConstants.UNDEFINED.match(buf))return JSConstants.UNDEFINED;
			if(JSConstants.NULL.match(buf))return JSConstants.NULL;
			err("Literal expected");
		}
		if(literal instanceof StringLiteralToken){
			return new JSString((StringLiteralToken)literal);
		}
		if(literal instanceof NumberLiteralToken){
			return new JSNumber((NumberLiteralToken)literal);
		}
		if(literal instanceof RegexpLiteralToken){
			return new JSRegExp((RegexpLiteralToken)literal);
		}
		throw new JSParseException("Unknow literal type");
	}
	
	protected boolean isKeyword(CharSequence kw){
		int len=kw.length();
		int i=pos;
		int x=0;
		while(x<len){
			if(i>=data.length||
				kw.charAt(x)!=data[i])return false;
			
			i++;x++;
		}
		if(i<data.length&&
			JS_INDEFIER.match(data[i]))return false;
		
		return true;
	}
	
	protected void skip(int count){
		pos+=count;
	}
	
	protected void assertEnd(){
		if(pos==0)return;
		if(pos<data.length){
			while(data.length>pos&&
					CharPattern.ALLOW_WHITE_SPACES
					.match(data[pos]))pos++;
		}
		if(pos<data.length&&data[pos]!=';'){
			err("Semicolon expected", token.getLine());
		}
	}
	
	protected void setHandler(JSErrorHandler handler){
		this.handler = handler;
	}
	
	protected JSErrorHandler getHandler(){
		return handler;
	}
	
	protected boolean is(char c){
		return data[pos]==c;
	}
	
	protected boolean match(CharPattern regex){
		return data!=null&&regex.match(data[pos]);
	}
	
	protected char incr(){
		return data[pos++];
	}
	
	protected boolean isLine(){
		return pos<data.length;
	}
	
	protected void warn(String message){
		warn(message,token.getLine());
	}
	
	protected void warn(String message, int line) {
		if(handler==null)return;
		handler.warn(message, line);
	}
	
	protected void err(String message){
		err(message,token.getLine());
	}
	
	protected void err(String message, int line) {
		//TODO: remove this check to make error handling more clear:
		if(handler==null)return;
		handler.error(message, line);
	}
}
